﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Reflection;

using ApiDoc.Extensions;

namespace ApiDoc
{
    partial class ApiDocumentationManager
    {
        /// <summary>
        /// Creates a string version of the documentation with each potential Cref link wrapped in
        /// &lt;link cref="the-ref"&gt;Word&lt;/link&gt;.
        /// </summary>
        /// <returns></returns>
        public virtual string CreateDocumentationStringWithLinks(string input, IMemberDocumentation member, bool lookupAllWords = false)
        {
            // Find the owning type (if any)
            MemberInfo memberInfo = null;
            Type type = null;
            if (member is TypeDocumentation) {
                type = ((TypeDocumentation)member).Member;
            } else if (member is PropertyDocumentation) {
                type = ((PropertyDocumentation)member).Member.ReflectedType;
                memberInfo = ((PropertyDocumentation)member).Member;
            } else if (member is MethodDocumentation) {
                type = ((MethodDocumentation)member).Member.ReflectedType;
                memberInfo = ((MethodDocumentation)member).Member;
            } else if (member is FieldDocumentation) {
                type = ((FieldDocumentation)member).Member.ReflectedType;
                memberInfo = ((FieldDocumentation)member).Member;
            } else if (member is ConstructorDocumentation) {
                type = ((ConstructorDocumentation)member).Member.ReflectedType;
                memberInfo = ((ConstructorDocumentation)member).Member;
            } else if (member is AssemblyDocumentation) {
                // TODO
            } else if (member is NamespaceDocumentation) {
                // TODO
            }

            if (type == null) {
                // Give up and return input.
                return input;
            }

            // Start with the idea of returning the input unchanged.
            var ret = input;

            // Perform a paramref replacements
            var paramRefs = Regex.Matches(input, @"<paramref\s+name=""([^""]*)""\s*/>")
                    .Cast<Match>()
                    .ToList();
            foreach (var paramRef in paramRefs) {
                string crefType = String.Empty;
                var cref = FindCref(paramRef.Groups[1].Value, type, memberInfo, out crefType);
                if (!String.IsNullOrEmpty(cref)) {
                    string replacement = String.Format("<link type=\"{2}\" cref=\"{1}\">{0}</link>", paramRef.Groups[1].Value, cref, crefType);
                    ret = ret.Replace(paramRef.Value, replacement);
                }
            }

            // Perform a typeparamref replacements
            paramRefs = Regex.Matches(input, @"<typeparamref\s+name=""([^""]*)""\s*/>")
                    .Cast<Match>()
                    .ToList();
            foreach (var paramRef in paramRefs) {
                string crefType = String.Empty;
                var cref = FindCref(paramRef.Groups[1].Value, type, memberInfo, out crefType);
                if (!String.IsNullOrEmpty(cref)) {
                    string replacement = String.Format("<link type=\"{2}\" cref=\"{1}\">{0}</link>", paramRef.Groups[1].Value, cref, crefType);
                    ret = ret.Replace(paramRef.Value, replacement);
                }
            }

            // If we have been asked to, look at every word and see if we can create a match to them.
            if (lookupAllWords) {
                var words = Regex.Matches(input, @"(\w)")
                    .Cast<Match>()
                    .Select(item => item.Value)
                    .Distinct()
                    .ToList();
                foreach (string word in words) {
                    string crefType = String.Empty;
                    var cref = FindCref(word, type, memberInfo, out crefType);
                    if (!String.IsNullOrEmpty(cref)) {
                        string replacement = String.Format("<link type=\"{2}\" cref=\"{1}\">{0}</link>", word, cref, crefType);
                        ret = ret.Replace(word, replacement);
                    }
                }
            }

            return ret;
        }

        /// <summary>
        /// Try and find a Cref matching <paramref name="input"/> relative to <paramref name="type"/>.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public virtual string FindCref(string input, Type type, MemberInfo member, out string crefType)
        {
            // Special handling for the word "base".
            if (input == "base") {
                crefType = "Type";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(type.BaseType);
            }

            // If the name matches the type itself then return a link to the type.
            if (input == type.Name) {
                crefType = "Type";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(type);
            }

            // Is this a parameter or generic type of a method.
            if (member is MethodInfo) {
                var method = (MethodInfo)member;
                var parameterInfo = method.GetParameters()
                    .Where(item => item.Name == input)
                    .FirstOrDefault();
                if (parameterInfo != null) {
                    // Closest we can get with a cref is the method.
                    crefType = "Method";
                    return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(method);
                }

                if (method.IsGenericMethodDefinition) {
                    var genericTypeInfo = method.GetGenericArguments()
                        .Where(item => item.Name == input)
                        .FirstOrDefault();

                    if (genericTypeInfo != null) {
                        // Closest we can get with a cref is the method.
                        crefType = "Method";
                        return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(method);
                    }
                }
            } else if (member is ConstructorInfo) {
                var method = (ConstructorInfo)member;
                var parameterInfo = method.GetParameters()
                    .Where(item => item.Name == input)
                    .FirstOrDefault();
                if (parameterInfo != null) {
                    // Closest we can get with a cref is the method.
                    crefType = "Method";
                    return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(method);
                }

                if (method.IsGenericMethodDefinition) {
                    var genericTypeInfo = method.GetGenericArguments()
                        .Where(item => item.Name == input)
                        .FirstOrDefault();

                    if (genericTypeInfo != null) {
                        // Closest we can get with a cref is the method.
                        crefType = "Method";
                        return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(method);
                    }
                }
            }

            // Is this a type parameter.
            if (type.IsGenericTypeDefinition) {
                var genericTypeInfo = type.GetGenericArguments()
                        .Where(item => item.Name == input)
                        .FirstOrDefault();

                if (genericTypeInfo != null) {
                    // Closest we can get with a cref is the type.
                    crefType = "Type";
                    return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(type);
                }
            }

            // Is this a property.
            var propertyInfo = type.GetProperties(System.Reflection.BindingFlags.FlattenHierarchy)
                .Where(item => item.Name == input)
                .FirstOrDefault();

            if (propertyInfo != null) {
                crefType = "Property";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(propertyInfo);
            }

            // Is this a field.
            var fieldInfo = type.GetFields(System.Reflection.BindingFlags.FlattenHierarchy)
                .Where(item => item.Name == input)
                .FirstOrDefault();
            if (fieldInfo != null) {
                crefType = "Field";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(fieldInfo);
            }

            // Is this a method.
            var methodInfo = type.GetMethods(System.Reflection.BindingFlags.FlattenHierarchy)
                .Where(item => item.Name == input)
                .FirstOrDefault();
            if (methodInfo != null) {
                crefType = "Method";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(methodInfo);
            }

            // Is this a nested type.
            var matchingType = type.GetNestedTypes(System.Reflection.BindingFlags.FlattenHierarchy)
                .Where(item => item.Name == input)
                .FirstOrDefault();
            if (matchingType != null) {
                crefType = "Type";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(matchingType);
            }

            // Is this a parent type.
            if (type.IsNested && input == type.ReflectedType.Name) {
                matchingType = type.ReflectedType;
                if (matchingType != null) {
                    crefType = "Type";
                    return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(matchingType);
                }
            }

            // Try and find a matching type in the assembly of type.
            matchingType = type.Assembly.GetTypesSafe()
                .Where(item => item.Name == input)
                .OrderByDescending(item => ScoreForCloseness(item, type))
                .FirstOrDefault();

            if (matchingType != null) {
                crefType = "Type";
                return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(matchingType);
            }

            // Try and find a matching type in any loaded assembly.
            foreach (var assembly in GetAssemblies().Where(item => item != type.Assembly).ToList()) {
                matchingType = type.Assembly.GetTypesSafe()
                    .Where(item => item.Name == input)
                    .OrderByDescending(item => ScoreForCloseness(item, type))
                    .FirstOrDefault();

                if (matchingType != null) {
                    crefType = "Type";
                    return CRef.ReflectionCRefGenerator.NoPrefix.GetCRef(matchingType);
                }
            }

            // TODO should we search the base System namespace here?

            // If we get here give up.
            crefType = String.Empty;
            return null;
        }

        /// <summary>
        /// Scores two classes by looking at their namespace parts and returning a point for each part of the namespace that is the same.
        /// </summary>
        /// <returns></returns>
        private int ScoreForCloseness(Type type1, Type type2)
        {
            string[] type1Parts = type1.Namespace.Split('.');
            string[] type2Parts = type2.Namespace.Split('.');

            for (int i = 0; i < type1Parts.GetLength(0) && i < type2Parts.GetLength(0); ++i) {
                if (type1Parts[i] != type2Parts[i]) {
                    return i;
                }
            }

            return type1Parts.GetLength(0); // Full match.
        }
    }
}
